Skip to content

fix(supabase): document the second entry point, and pin the runtime to the engine - #952

Merged
tobyhede merged 2 commits into
mainfrom
docs/supabase-reference-two-entries
Aug 31, 2026
Merged

fix(supabase): document the second entry point, and pin the runtime to the engine#952
tobyhede merged 2 commits into
mainfrom
docs/supabase-reference-two-entries

Conversation

@tobyhede

@tobyhede tobyhede commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

@cipherstash/stack-supabase wraps a Supabase client so that columns you have marked as encrypted are encrypted on the way into the database and decrypted on the way out. It has two entry points: the default one, which uses a native Node module to do the encryption, and @cipherstash/stack-supabase/wasm-inline, which uses a WebAssembly build instead so it can run on edge runtimes — Deno, Supabase Edge Functions, Cloudflare Workers.

Our reference sheet, and the hover documentation that ships to users inside the package's type definitions, both explained why the default entry only runs on Node. The explanation was wrong. They said it was because the wrapper reads your table definitions out of the database at startup ("introspection"), which needs a direct Postgres connection. The real reason is the encryption engine it imports.

That matters because you can turn introspection off. Passing a schemas option makes the wrapper use the table definitions you hand it instead of reading them from the database, and then no Postgres connection is opened at all. Follow the old explanation and you would conclude that doing so lets you deploy the default entry to a Cloudflare Worker. It does not — it fails when the module is imported, before any of your code runs. The fix is to import the wasm-inline entry, which is a different decision entirely.

This started as a docs-only change to the reference sheet and grew: the same wrong explanation turned out to be in three source files whose comments ship to users as .d.ts type definitions, so the PR now carries a patch changeset and two guard tests.

Changes

Reference sheet (docs/reference/supabase-sdk.md)

  • The default entry's Node-only restriction is now attributed to the engine it imports, not to introspection, and says explicitly that declaring schemas does not move it.
  • "Cannot run in a Worker" now names the edge runtimes. Bare "Worker" is ambiguous and false under one reading — the default entry runs fine inside a Node worker_threads worker.
  • Restores the browser prohibition to the default entry. The previous revision of this branch moved it onto the edge entry, which left a reader of the default entry's paragraph with no browser warning at all.
  • Drops "they differ only in how the wrapper learns the schema, and therefore in where it can run", which contradicted the entry-point table two lines above it.

Shipped hover documentation (ships as .d.ts, hence the changeset)

  • src/create.ts — the comment on the exported factory said "declare your schemas and it runs anywhere". Replaced with "the entry point decides where this runs; schemas decides only whether Postgres is involved."
  • src/index.ts and src/create.ts both named @cipherstash/protect-ffi as the native module that loads on import. It is the one package in that graph that deliberately does not load on import; the module-evaluation-time load belongs to @cipherstash/auth. Corrected in both, with the second reason named too: the default entry's bundle carries an import("pg") specifier that a bundler resolves at build time.
  • src/wasm-inline.ts — dropped "introspection is half of what made the default entry Node-only".

Guards

  • New scripts/__tests__/supabase-runtime-claims.test.mjs: three prose detectors, each unit-tested in both directions, applied to the four files above.
  • packages/stack-supabase/__tests__/wasm-entry-edge-safety.test.ts gains three assertions tying the corrected prose to the emitted bundles. Its header comment claimed the stack root pulls both native packages but asserted nothing, so the negative assertions below it could have gone vacuous without failing. That comment repeated the same misattribution and is corrected.

Changeset@cipherstash/stack-supabase patch. Verified the corrected comments actually reach the published artifact: after a rebuild, dist/index.d.ts:535 carries the new text.

Verification

  • pnpm run test:scripts — 854 passed, 50 files. Includes the new guard (37 tests).
  • pnpm --filter @cipherstash/stack-supabase test — 568 passed, 16 files.
  • The guard was proved fallible rather than assumed to work. Reintroducing "Introspection needs a direct Postgres connection, so this entry cannot run in a Worker" into the reference sheet turns it red on two assertions — the false causal link and the bare "Worker" — and restoring the text returns 37/37.
  • The three new bundle assertions were each proved fallible by mutation: dropping the auth import from the stack root, unwrapping the platform requires in protect-ffi/src/load.cts, and reverting index.cts to import * as native each fail a distinct assertion. Tree restored and verified clean afterwards.
  • The guard runs in CI. scripts/__tests__/**/*.test.mjs is picked up by pnpm run test:scripts, invoked at .github/workflows/tests.yml:296 in the lint job on every pull request.
  • Biome reports 0 errors on every changed file. Caveat, stated rather than omitted: pnpm run code:check repo-wide currently fails on the author's machine for an unrelated reason — ten stale .claude/worktrees/agent-*/biome.json files trigger a "nested root configuration" error. Those are excluded via .git/info/exclude rather than .gitignore, so a fresh CI checkout never sees them. Scoped to the real directories (biome check packages scripts docs e2e examples skills biome-plugins, 861 files) it is 0 errors and 201 pre-existing warnings.

The underlying claims were checked against source, not assumed: packages/protect-ffi/src/index.cts:6-18 (deliberately defers its binary load, guarded by nativeLoading.test.ts), packages/stack/src/index.ts:24 (the static @cipherstash/auth import), and packages/stack-supabase/src/create.ts:303-306 and :364-367 (declaring schemas skips introspection entirely).

Related

Refs #912, #804, #708, #950.

Review notes

Start with docs/reference/supabase-sdk.md and src/create.ts — the latter is the comment users actually see on hover, so it is the highest-leverage correction here.

Deliberately not fixed, to avoid conflicting with open PRs:

One thing worth raising on #951 rather than here: its rewrite of those README lines keeps the same false "so" and drops the browser half. Cheaper to fix the sentence there before it lands than to let this guard bounce it afterwards.

…one runs in a Worker

`docs/reference/supabase-sdk.md` still described the state before #912: one
entry point, and a factory that "cannot run in a Worker or the browser".

#912 added `@cipherstash/stack-supabase/wasm-inline`, which carries no Postgres
driver and takes declared `schemas` instead of introspecting. Introspection was
the only thing that needed a Postgres socket, so that entry does run in a
Worker. The reference never said so.

Corrects both halves:

- the "One entry point" table now lists both, with the engine, how each learns
  the schema, and where each runs;
- the factory paragraph now scopes its restriction to the native entry and
  points at the edge entry as the way to run in a Worker.

The browser half of the old sentence was correct and is kept, with the reason
named: the WASM client requires a workspace `clientKey` on every auth path
(#804).

Internal reference documentation — no package ships `docs/`, so no changeset.
The same stale sentence is still live in `skills/stash-supabase/SKILL.md` and
`packages/stack-supabase/README.md`, both of which DO ship; those are tracked
separately.

Claude-Session: https://claude.ai/code/session_01E1J2nVGJWVkqvLepDfinRf
@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f31a202

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cipherstash/stack-supabase Patch
stash Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-prisma Patch
@cipherstash/wizard Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

The reference doc and three TSDoc blocks all derived the default entry's
runtime from schema discovery: introspection needs Postgres, therefore the
entry cannot run on an edge runtime. That inference is false in both
directions. Declaring `schemas` removes the Postgres dependency entirely
(create.ts:303-306, :364-367) and the entry is still Node-only; and the
entry would be Node-only with no introspection code in it at all.

What actually pins it is the import: `Encryption` from `@cipherstash/stack`
pulls a module graph that statically imports `@cipherstash/auth`, whose Node
entry resolves its platform binding at module evaluation, and the emitted
bundle carries an `import("pg")` specifier a bundler resolves at build time.
Neither moves when you declare schemas.

The default entry's doc also named `@cipherstash/protect-ffi` as the binary
loaded on import. It is the one package in that graph that deliberately does
not: `packages/protect-ffi/src/index.cts` uses `import native = require(...)`
specifically so `__importStar` cannot force the neon proxy to resolve, and
`nativeLoading.test.ts` guards it.

Two smaller corrections in the same pass: bare "a Worker" is ambiguous and
false under the Node `worker_threads` reading — the native entry runs fine
there — so the edge runtimes are now named, as the table already named them;
and the browser prohibition is restored to the native entry, which the
previous revision moved onto the edge entry, leaving the native paragraph
implying the browser was fine.

Guarded by scripts/__tests__/supabase-runtime-claims.test.mjs (three
detectors, unit-tested in both directions, applied to the four prose
sources), and by three new assertions in wasm-entry-edge-safety.test.ts that
tie the corrected prose to the emitted bundles — its header comment repeated
the protect-ffi misattribution and would otherwise have contradicted them.

Not touched, to avoid conflicting with open PRs: skills/stash-supabase and
packages/stack-supabase/README.md carry defect 1 verbatim but are being
rewritten on #951 at those exact lines, and the browser-capability claims in
examples/ and packages/stack/tsup.config.ts belong to #953. The README path
is recorded in the guard's GUARDED list comment so it is added when #951
lands.

Claude-Session: https://claude.ai/code/session_01FVKXa6GjUHN5xvJq2912KA
@tobyhede
tobyhede requested a review from coderdan August 31, 2026 00:15
@tobyhede
tobyhede marked this pull request as ready for review August 31, 2026 00:15
@tobyhede
tobyhede requested a review from a team as a code owner August 31, 2026 00:15

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the corrections are right, I verified each load-bearing claim against the tree, and the guards are built the way this repo builds guards (detectors unit-tested in both directions, failure messages that say what to rewrite). One ask on the PR description below.

What I verified

  • The protect-ffi/auth attribution is correct. packages/protect-ffi/src/index.cts:17 uses import native = require('./load.cjs'), load.cts wraps every platform require in an arrow handed to @neon-rs/load, and nativeLoading.test.ts exists and guards it. So the import-time dlopen in the native graph does belong to @cipherstash/auth, and the old TSDoc naming protect-ffi was wrong exactly as the changeset says.
  • The new build-output gate points at a real file. @cipherstash/stack's exports["."].import.default is ./dist/index.js, which is what STACK_ROOT_ENTRY resolves — so describeStackBuilt is a genuine skip-until-built, not a never-runs path. (Given this PR's own lecture on vacuous guards, that was the first thing I checked.)
  • The prose guard actually runs in CI: scripts/__tests__/*.test.mjs is wired through test:scripts alongside the existing guard suite.
  • code() — used by the new auth-chain assertions — exists in the edge-safety test on this branch (line 40).
  • The GUARDED-list note about README.md is accurate, not stale: #951 is still open, so deferring the README path to post-merge avoids the two-branch conflict as described.
  • CI: everything green; the skips are the EQL path-filtered jobs, expected for a PR that doesn't touch packages/eql.

The "unwrap inline code spans instead of deleting them" note in prose() is a nice catch — deleting spans would have disarmed the protect-ffi detector on every file, since the package name only ever appears in backticks.

The one ask: the PR description contradicts the diff

The Summary and Verification sections still describe the docs-only version of this branch: "docs/ ships in no package … so there is no changeset and nothing to rebuild" and "This PR deliberately does not touch them: they need changesets and a rebuild, and this one needs neither." The PR now ships a @cipherstash/stack-supabase patch changeset, TSDoc changes in three src/ files (which ship as .d.ts), and two guard tests. The changeset itself is accurate — it's the body that's frozen at an earlier scope. Please refresh it before merging; this repo mines PR bodies for provenance, and this one currently asserts the absence of a file it contains.

Non-blocking, for #951 rather than here

The GUARDED comment notes #951's branch "keeps the false so while dropping the browser half" — i.e. the causal inversion corrected here is about to land again in the README via #951, to be caught only when the README path is added to this guard afterwards. Cheaper to fix the sentence on the #951 branch now than to land it wrong and let the guard bounce it; worth a comment over there.

@tobyhede tobyhede changed the title docs(reference): the Supabase SDK has two entry points, and the edge one runs in a Worker fix(supabase): document the second entry point, and pin the runtime to the engine Aug 31, 2026
@tobyhede
tobyhede merged commit 814d68c into main Aug 31, 2026
27 checks passed
@tobyhede
tobyhede deleted the docs/supabase-reference-two-entries branch August 31, 2026 01:28
tobyhede added a commit that referenced this pull request Aug 31, 2026
…t introspection

The README and the Supabase skill both wrote "introspection needs a direct
Postgres connection, so ... this entry runs on Node only". Introspection is not
what makes the entry Node-only — it binds the native engine, and declaring
schemas does not move that. The stated cause invites the conclusion that
declaring tables makes the root entry edge-capable, which is the wrong turn the
wasm-inline entry exists to prevent. This is the same causal inversion #952
corrected in the package's .d.ts.

Two smaller instances in skills/stash-supabase/SKILL.md go with it: a bare
"Worker" where the surrounding paragraph means any edge runtime, and the
co-cause "and the engine is a native module, so ..." split into its own
sentence so the engine carries the claim alone.

Claude-Session: https://claude.ai/code/session_01133kxi8qXu6UJPiKJDwDt6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants